package fr.alma.domo.model;

import java.io.Serializable;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

import fr.alma.domo.util.Log;

@SuppressWarnings("serial")
public abstract class Resource implements Serializable{
	
	/**
	 * Attribut permettant de vérifier si la ressource est instance d'une classe
	 */
	@Property(type=Boolean.class,readonly=true)
	public final static String INSTANCE_OF="Resource#instanceOf";
	
	/**
	 * Défini le type de la ressource
	 */
	@Property(type=String.class)
	public final static String SCOPE="Resource#family";
	
	/**
	 * Registre de tous les services créés, classé par type
	 */
	private static final Map<String,List<Resource>> REGISTRY = new HashMap<String, List<Resource>>();

	/**
	 * 
	 * @return La liste de toutes les ressources du système
	 */
	public final static Map<String,List<Resource>> getRegistry(){
		return Collections.unmodifiableMap(REGISTRY);
	}
	
//	/**
//	 * La liste de toute les observateurs de la ressource
//	 */
//	private final List<ResourceListener> listeners = new ArrayList<ResourceListener>();
		
//	/**
//	 * L' état de la ressource et défini par des attribus
//	 */
//	private final Map<String,Attribute> state = new HashMap<String, Attribute>();
	
	/**
	 * Identifiant unique de la resource
	 */
	protected final UUID rid = UUID.randomUUID();
	
	/**
	 * Si true la resource notify en cas de modification d'un de ses attributs
	 */
	private boolean couldNotify = true;
	
	/**
	 * Etat de toutes les resources
	 */
	private final static Map<UUID,Map<String,Attribute>> STATES = Collections.synchronizedMap(new HashMap<UUID, Map<String,Attribute>>());
	private final static Map<UUID,List<ResourceListener>> LISTENERS = Collections.synchronizedMap(new HashMap<UUID, List<ResourceListener>>());
	
	/**
	 * Pour gérer les concurrences dans les notifications on rajoute
	 * deux listes temporaires l'une comportant les listeners à enlever et 
	 * l'autre les listeners à rajouter. Le retrait et l'ajout de listeners
	 * s'effectuera avant chaque notification
	 */
	private final static Map<UUID,List<ResourceListener>> LISTENERS_TO_REMOVE = Collections.synchronizedMap(new HashMap<UUID, List<ResourceListener>>());
	private final static Map<UUID,List<ResourceListener>> LISTENERS_TO_ADD = Collections.synchronizedMap(new HashMap<UUID, List<ResourceListener>>());
	
	/**
	 * Une ressource est construite en précisant sont type.
	 * Tous les champs de la resource portant l' annotation
	 * {@code fr.alma.domo.model.Property} seront transformé
	 * en attribut de la ressource.
	 * 
	 * @param family Le type
	 */
	protected Resource(String family){
		_setState(new HashMap<String, Attribute>());
		LISTENERS.put(rid,Collections.synchronizedList(new ArrayList<ResourceListener>()));
		LISTENERS_TO_ADD.put(rid,Collections.synchronizedList(new ArrayList<ResourceListener>()));
		LISTENERS_TO_REMOVE.put(rid,Collections.synchronizedList(new ArrayList<ResourceListener>()));
		
		//On charge tous les attributs de la classe
		Class<?> cls = getClass().getSuperclass();
		while(cls!=null){
			_loadPropertiesFrom(cls);
			cls=cls.getSuperclass();
		}

		_loadPropertiesFrom(getClass());	
		for(Class<?> c : getClass().getInterfaces()){
			_loadPropertiesFrom(c);
		}

		setAttribute(SCOPE,family);
		_store();
		
		addListener(EventHandler.INSTANCE);
	}

	@Override
	protected void finalize() throws Throwable{
		try{
			_del();
//			removeListener(EventHandler.INSTANCE);
//			_listeners().clear();
//			_listenersToAdd().clear();
//			_listenersToRemove().clear();
//			LISTENERS.remove(rid);
//			LISTENERS_TO_ADD.remove(rid);
//			LISTENERS_TO_REMOVE.remove(rid);
		}
		finally{
			super.finalize();
		}
	}
	
	/**
	 * S' enregistre dans le registre des ressources
	 */
	private final void _store(){		
		String family = getAttribute(SCOPE,String.class);
		List<Resource> l = getRegistry().get(family);
		if(l==null){
			l = new ArrayList<Resource>();
		}
		l.add(this);
		REGISTRY.put(family, l);
	}
	
	/**
	 * Se désenregistre du registre des ressources
	 */
	private final void _del(){
		String family = getAttribute(SCOPE,String.class);
		List<Resource> l = getRegistry().get(family);
		if(l!=null){
			l.remove(this);
			getRegistry().put(family, l);
		}
	}
	
	private List<ResourceListener> _listeners(){
		return LISTENERS.get(rid);
	}

	private List<ResourceListener> _listenersToRemove(){
		return LISTENERS_TO_REMOVE.get(rid);
	}

	private List<ResourceListener> _listenersToAdd(){
		return LISTENERS_TO_ADD.get(rid);
	}
	
	private final Map<String,Attribute> _getState(){
		return STATES.get(rid);
	}
	
	private final void _setState(Map<String,Attribute> state){
		STATES.put(rid,state);
	}
	
	/**
	 * Enregistre comme attribut de la ressource, tous les attributs de la classe ayant
	 * portant l'annotation <code>fr.alma.domo.model.Property</code>
	 * @param c La classe
	 */
	private final void _loadPropertiesFrom(Class<?> c){
		for(Field f : c.getDeclaredFields()){
			if(f.isAnnotationPresent(Property.class)){
				Property prop = f.getAnnotation(Property.class);
				if(f.getType()!=String.class){
					throw new IllegalPropertyException("The 'Property' annotation is only allowed on string fields");
				}
				f.setAccessible(true);
				try {
					declareAttribute(""+f.get(this),prop.type(),prop.readonly(),prop.fragile());
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	/**
	 * Ajouter un attrbut à la ressource
	 * @param <T> Le type de l' attribut
	 * @param attributeName Le nom de l' attribut
	 * @param type La classe representant le type de l'attribut
	 * @param readonly Attribut en lecture seule
	 * @throws <code>DuplicateAttributeException</code> si un attribut du meme nom était déja déclaré
	 */
	protected final<T> void declareAttribute(String attributeName,Class<T> type,boolean readonly,boolean fragile){
		if(_getState().containsKey(attributeName)){
			throw new DuplicateAttributeException(attributeName+" is already defined");
		}
		Attribute a = new Attribute(attributeName,type);
		a.setReadonly(readonly);
		a.setFragile(fragile);
		_getState().put(attributeName,a);
	}
	
	/**
	 * Mofidier la valeur d'un attribut
	 * @param <T> Le type de l'attribut
	 * @param attributeName Le nom de l'attribut
	 * @param value La nouvelle valeur de l'attribut
	 * @throws <code>IllegalAttributException</code> :<p>
	 * si le type l'attribut ne correspond pas au type déclaré
	 * </p>
	 */
	public final<T> void setAttribute(String attributeName,T value){	
		String family = getAttribute(SCOPE,String.class);
		Attribute a = _getState().get(attributeName);		
		if(a!=null){
			if(a.isReadonly()){
				//Aucune valeur a setter, on notifie tout de meme
				notifyPropertyChange(attributeName, null, value);
			}
			else{
				try{
					Object oldValue = a.getValue();
					a.setValue(a.getType().cast(value));
					Log.i(getClass().getName()+"["+rid+"]", "Setting attribute [%s] to [%s]", attributeName,a.getValue());
					notifyPropertyChange(attributeName, oldValue, a.getValue());
				}
				catch(ClassCastException e){
					throw new IllegalAttributeException(String.format("Bad value type for attribute %s.%s expecting <%s> got <%s>",family,a.getName(),a.getType().getSimpleName(),value.getClass().getSimpleName()),e);
				}
			}
		}
		else{
			throw new NoSuchAttributeException(attributeName);
		}
	}
	
	/**
	 * Récupérer la valeur d'un attribut
	 * @param <T> Le type de l'attribut
	 * @param attributeName Le nom de l'attribut
	 * @param type La classe correspondant au type de l'attribut
	 * @return La valeur attribut trouvé
	 * @throws <code>NoSuchAttributeException</code> si l'attribut n'existe pas
	 * @throws <code>IllegalAttributeException</code> si le type de l'attribut new correspond pas au parametre <code>type</code>
	 */
	@SuppressWarnings("unchecked")
	public final<T> T getAttribute(String attributeName,Class<T> type){
		String familly = (String)_getState().get(SCOPE).getValue();
		if(INSTANCE_OF.equals(attributeName)){
			if(type==boolean.class || type==Boolean.class){
				Object o = type.isInstance(this);
				return (T)o;
			}
			throw new IllegalAttributeException(String.format("Bad value type for attribute %s.%s expecting <boolean> got <%s>",familly,INSTANCE_OF,type.getSimpleName()));			
		}
		
		Attribute a = _getState().get(attributeName);
		if(a==null){
			throw new NoSuchAttributeException(String.format("Unidentified resource field %s.%s",familly,attributeName));
		}
		try{
			return (T)type.cast(a.getValue());
		}
		catch(ClassCastException e){
			throw new IllegalAttributeException(String.format("Bad value type for attribute %s.%s expecting <%s> got <%s>",familly,a.getName(),a.getType().getSimpleName(),type.getSimpleName()),e);
		}
	}

	/**
	 * 
	 * @param attributeName
	 * @return
	 */
	public final Object getAttribute(String attributeName){		
		Attribute a = _getState().get(attributeName);
		if(a==null){
			throw new NoSuchAttributeException(String.format("Unidentified resource field %s.%s",getAttribute(SCOPE,String.class),attributeName));
		}
		return getAttribute(attributeName,a.getType());
	}
	
	/**
	 * Vérifier la ressource correspond a une description
	 * @param desc La description
	 * @return <code>true</code> si la resource correspond a la description
	 */
	public final boolean matches(Description desc){
		String family = getAttribute(SCOPE,String.class);
		if(desc.getScopes().contains(family)){
			Properties p = desc.getProperties();
			for(Object key : p.keySet()){
				if(INSTANCE_OF.equals(key)){
					Object obj = p.get(key);
					if(obj instanceof Class<?>){
						if(!((Class<?>)obj).isInstance(this)){
							return false;
						}
					}
					else{
						return false;
					}
				}
				else if(hasAttribute(""+key)){
					Object got = getAttribute((String)key, Object.class);
					Object expected = p.get(key);
					if(got==null && expected!=null){
						return false;
					}
					else if(!got.equals(expected)){
						return false;
					}
				}
				else{
					return false;
				}
			}
			
			p = desc.getSequenceProperties();
			for(Object key : p.keySet()){
				if(!hasAttribute(""+key)){
					return false;
				}
				Collection<?> got = getAttribute((String)key, Collection.class);
				Object expected = p.get(key);
				if(got==null || !got.contains(expected)){
					return false;
				}
			}
			
			return true;
		}
		return false;
	}
	
	/**
	 * 
	 * @return La liste des propriétés de la resource
	 */
	public final List<String> listAttributes(){
		return new ArrayList<String>(_getState().keySet());
	}
		
	/**
	 * Vérifie que la ressource possède un attribut
	 * @param name
	 * @return
	 */
	public final boolean hasAttribute(String name){
		return name!=null && _getState().containsKey(name);
	}

	/**
	 * 
	 * @param attributeName
	 * @return La valeur de la propriété <code>readonly</code> de l'attribut
	 * @throws  NoSuchAttributeException si aucun attribut ne portent le nom fournis
	 */
	public final boolean isAttributeReadOnly(String attributeName) {
		Attribute a = _getState().get(attributeName);
		if(a==null){
			throw new NoSuchAttributeException(String.format("Unidentified resource field %s.%s",getAttribute(SCOPE,String.class),attributeName));
		}
		return a.isReadonly();
	}

	/**
	 * 
	 * @param attributeName
	 * @return La valeur de la propriété <code>fragile</code> de l'attribut
	 * @throws  NoSuchAttributeException si aucun attribut ne portent le nom fournis
	 */
	public final boolean isAttributeFragile(String attributeName) {
		Attribute a = _getState().get(attributeName);
		if(a==null){
			throw new NoSuchAttributeException(String.format("Unidentified resource field %s.%s",getAttribute(SCOPE,String.class),attributeName));
		}
		return a.isFragile();
	}
	
	/**
	 * 
	 * @param listener
	 */
	public final void addListener(ResourceListener listener){
		if(listener!=null && !this._listenersToAdd().contains(listener) && !this._listeners().contains(listener)){
			this._listenersToAdd().add(listener);
			this._listenersToRemove().remove(listener);
		}
	}
	
	/**
	 * 
	 * @param listener
	 */
	public final void removeListener(ResourceListener listener){
		if(listener!=null){
			this._listenersToRemove().add(listener);
			this._listenersToAdd().remove(listener);
		}
	}
	
	/**
	 * 
	 * @param property
	 * @param oldValue
	 * @param newValue
	 */
	private final void notifyPropertyChange(String property,Object oldValue,Object newValue){
		if(couldNotify){
			final List<ResourceListener> listeners = _listeners();
			listeners.removeAll(_listenersToRemove());
			_listenersToRemove().clear();
			listeners.addAll(_listenersToAdd());
			_listenersToAdd().clear();
			synchronized(listeners){
				int size = listeners.size();
				for(int i=0;i<size;++i){
					ResourceListener l = listeners.get(i);
					try{
						l.onPropertyChange(this, property, oldValue, newValue);
					}
					catch(Throwable t){
						t.printStackTrace();
					}
				}
			}
		}
	}

	/**
	 * Activer les nofitications des listenersen cas de changement de son etat
	 */
	public final void enableNotification(){
		couldNotify = true;
	}
	
	/**
	 * Desactiver les notifications des listeners 
	 */
	public final void disableNotification(){
		couldNotify = false;
	}
	
	@Override public String toString(){
		StringWriter writer = new StringWriter();
		writer.write("{");
		for(Attribute a : _getState().values()){
			writer.write(String.format("%s=[%s] ,",a.getName(),a.getValue()));
		}
		writer.write("}");
		return writer.toString();
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((rid == null) ? 0 : rid.hashCode());
		return result;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Resource other = (Resource) obj;
		if (rid == null) {
			if (other.rid != null)
				return false;
		} else if (!rid.equals(other.rid))
			return false;
		return true;
	}
	
	
}
